﻿using System;
using System.Linq;

namespace Cluster
{
    public abstract partial class ClusterManager
    {
        private bool balancingScheduled;    // was a balancing scheduled but not executed yet?
        
        /// <summary>
        /// Checks if balancing is required.
        /// If yes, schedules a balancing task - a new entry is added to the distributed
        /// prioritized tasks queue and executed only when in the top of this queue 
        /// </summary>
        private void ScheduleBalancing()
        {
            Debug("Checking if need to balance (hosting {0} replicas out of {1} total servers)",
                SelfNode.ReplicasNumber, Math.Max(1, ClusterView.Sum(cn => cn.ReplicasNumber)));

            // check if we need to schedule balancing - balancing was not already scheduled, we have more
            // replicas than the average in the cluster and we have a replica that can be transfered to
            // another node
            if (!balancingScheduled && GetNumberOfServersToBalance() > 0 && ChoseServerToBalance() != null)
            {
                try
                {
                    balancingScheduled = true;
                    replicationQueue.RegisterTask(ReplicationQueue.TaskType.Balance);
                    
                    Debug("Scheduling balancing");
                }
                catch (Exception)
                {
                    balancingScheduled = false;
                    throw;
                }
            }
            else
            {
                Debug("No balancing required");
            }
        }

        /// <summary>
        /// Performs the actual balancing
        /// </summary>
        private void DoBalancing()
        {
            Debug("Starting balancing method");

            balancingScheduled = false;

            // find a "victim" node with the least number of replicas
            var cn = ChoseBalancingNode();
            if (cn == null)
            {
                Debug("No other server found in cluster, can't balance");
                return;
            }

            // find a server that we can transofer to this node
            var name = ChoseServerToBalance(cn);
            if (name == null)
            {
                Debug("Did not find a possible server to balance");
                return;
            }

            try
            {
                ReplicateServerToNode(name, cn);        // perform replication of the server to the node
                UnregisterReplicatedServerBySelf(name); // and remove the local replica from this node
            }
            catch (ClusterException)
            {
                // we can't handle cluster exceptions here but we do want to
                // "handle" any remote server exception
                throw;
            }
            catch (Exception)
            {
                // let's try again
                // before the next balancing task runs, we'll get an updated view
                // of the cluster and some dead nodes might go away

                ScheduleBalancing();
            }

            // we might need to balance more but let's do it in the next time-slice
            // a new balancing will be scheduled after OnReplicationTreeChange is called (it
            // will be called if we succesfully replicated using ReplicateServerToNode)
        }

        /// <summary>
        /// Calculates the number of "above average" replicas in this node
        /// </summary>
        /// <returns>0 if below (or equal) to the average in the cluster</returns>
        private int GetNumberOfServersToBalance()
        {
            var totalServers = ClusterView.Sum(cn => cn.ReplicasNumber);
            var totalNodes = ClusterView.Count;

            var maxServersToNode = (int) Math.Ceiling((double) totalServers / totalNodes);
            var res = Math.Max(0, SelfNode.ReplicasNumber - maxServersToNode);
            
            Debug("Number of servers to balance = " + res);

            return res;
        }

        /// <summary>
        /// Finds the node with the least number of replicas in the cluster, that is
        /// not this node
        /// </summary>
        /// <returns>null if we're the only node in cluster</returns>
        private ClusterNode ChoseBalancingNode()
        {
            var res = from cn in ClusterView
                      where cn != SelfNode
                      orderby cn.ReplicasNumber
                      select cn;

            return res.FirstOrDefault();
        }

        /// <summary>
        /// Finds a replica that can be transfered to another node.
        /// If the other node is specified, only server names that are
        /// not yet replicated by this node are returned.
        /// </summary>
        /// <param name="cn"></param>
        /// <returns>the replica server name or null if not found</returns>
        private string ChoseServerToBalance(ClusterNode cn = null)
        {
            // find a list of servers that can't be picked (already replicated by the other node)
            var path = cn != null ? cn.ZkPath : null;
            var alreadyReplicated = (from r in ReplicationView
                                     where r.ReplicatingNode == path
                                     select r.ServerName).ToList();

            // find valid replicas to transfer
            var replica = ReplicationView.FirstOrDefault(r =>
                r.ReplicatingNode == SelfNode.ZkPath &&             // we replicate this server
                r.ServerName != ServerName &&                       // this is not our replication
                alreadyReplicated.Contains(r.ServerName) == false); // the target node does not replicate this server already

            var res = replica != null ? replica.ServerName : null;

            if (res != null)
            {
                Debug("Chose {0} server to balance", res);
            }

            return res;
        }
    }
}
